<?php
/**
 * Copyright 2010 Dave Keen
 * http://www.actionscriptdeveloper.co.uk
 * 
 * This file is part of Flextrine.
 * 
 * Flextrine is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * and the Lesser GNU General Public License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 * 
 */

namespace Flextrine\Tools;

use Doctrine\ORM\Proxy\Proxy;

// TODO: Check that merging isn't doing loads of transparent lazy loading to retrieve the whole connected tree if it doesn't need to

class AssociationWalker {
	
	const MERGE = "merge"; // Replace a received entity graph with its merged equivalent
	const LOAD_PROXIES = "load_proxies"; // Prepare an entity graph for returning to Flextrine in EAGER
	const INSERT_PROXY_IDENTIFIERS = "insert_proxy_identifiers"; // Prepare an entity graph for returning to Flextrine in LAZY
	
	static public function mergeAssociations($entityOrArray, $em) {
		return self::walkAssociations(AssociationWalker::MERGE, $entityOrArray, $em);
	}
	
	static public function loadProxies($entityOrArray, $em) {
		return self::walkAssociations(AssociationWalker::LOAD_PROXIES, $entityOrArray, $em);
	}
	
	static public function insertProxyIdentifiers($entityOrArray, $em) {
		return self::walkAssociations(AssociationWalker::INSERT_PROXY_IDENTIFIERS, $entityOrArray, $em);
	}
	
	static private function walkAssociations($operation, $entityOrArray, $em) {
		if (is_array($entityOrArray)) {
			foreach ($entityOrArray as $entity)
				self::_doWalkAssociations($operation, $entity, $em);
		} else if ($entityOrArray instanceof \Doctrine\Common\Collections\Collection) {
			// TODO: this is for changeset collectionsdelete and collectionsupdate but not sure if we need them
		} else {
			self::_doWalkAssociations($operation, $entityOrArray, $em);
		}
		
		return $entityOrArray;
	}
	
	// THERE IS A PROBLEM HERE: __isUnserialized__ is not getting created on all objects at all times (in collections it seems to just get
	// created on the first element - in fact no custom attributes get put in collections I think?)
	
	static private function _doWalkAssociations($operation, $entity, $em, &$visited = array()) {
		// Mark $entity as visited
		$visited[spl_object_hash($entity)] = true;
		
		// Create an empty map to track which collections have been eager loaded
		$entity->__isCollectionInitializedMap__ = array();
		
		// Get the class metadata
		$class = $em->getClassMetadata(get_class($entity));
		
		// Walk through the associations
		foreach ($class->associationMappings as $assoc) {
			$sourceAttribute = $assoc->sourceFieldName;
			
			// Get the value of the association
			$relatedEntities = $class->reflFields[$sourceAttribute]->getValue($entity);
			
			// If there are any associations
			if ($relatedEntities) {
				// If the association is a collection
				if ($relatedEntities instanceof \Doctrine\Common\Collections\Collection) {
					// If we are lazy loading we want to replace the collection with null a) so AMFPHP doesn't try and read from the collection
					// whilst serializing causing the collection to load and b) so Flextrine knows that the collection hasn't been loaded
					// TODO: Also for merge (see comment at top)???
					if ($operation != AssociationWalker::MERGE && ($relatedEntities instanceof \Doctrine\ORM\PersistentCollection && !$relatedEntities->isInitialized())) {
						if ($operation == AssociationWalker::INSERT_PROXY_IDENTIFIERS) {
							// Since the many attribute is not initialized replace by an empty ArrayCollection to avoid confusion
							$entity->$sourceAttribute = new \Doctrine\Common\Collections\ArrayCollection();
							
							// Make sure the last attribute is __isUnserialized__ so we can use this in Flextrine to determine whether an
							// object has finished unserializing or not.
							$entity->__isUnserialized__ = true;
						}
					} else {
						// If we reach here the collection is not a lazy loaded stub so add it to the initialized map
						$entity->__isCollectionInitializedMap__[$sourceAttribute] = true;
						
						// Go through the collection
						for ($n = 0; $n < $relatedEntities->count(); $n++) {
							$relatedEntity = $relatedEntities->get($n);
							// As long as we haven't already visited this entity
							if (!isset($visited[spl_object_hash($relatedEntity)])) {
								// And as long as it has an identifier
								if (sizeof($em->getClassMetadata(get_class($relatedEntity))->getIdentifierValues($relatedEntity)) > 0) {
									// Replace the unmanaged entity with its merged counterpart (if we are merging)
									if ($operation == AssociationWalker::MERGE) $relatedEntities->set($n, $em->merge($relatedEntity));
									// Recurse
									self::_doWalkAssociations($operation, $relatedEntity, $em, $visited);
								}
							}
						}
					}
					//$relatedEntities->_explicitType = "flex.messaging.io.ArrayCollection";
				} else {
					// Otherwise the association is a one-to-one
					$relatedEntity = $relatedEntities;
					
					if (!isset($visited[spl_object_hash($relatedEntity)])) {
						// As long as it has an identifier
						if (sizeof($em->getClassMetadata(get_class($relatedEntity))->getIdentifierValues($relatedEntity)) > 0)
							// Replace the entity with its merged counterpart (if we are merging)
							if ($operation == AssociationWalker::MERGE) $entity->$sourceAttribute = $em->merge($relatedEntity);
						
						// Load the proxy (if we are loading proxies)
						if ($operation == AssociationWalker::LOAD_PROXIES && $relatedEntity instanceof Proxy && !$relatedEntity->__isInitialized__) {
							// This ensures that all proxies are loaded fully before returning the tree and is used when Flextrine is in
							// EAGER mode.
							$reflectionClass = new \ReflectionClass($relatedEntity);
							
							$entityPersisterProperty = $reflectionClass->getProperty("_entityPersister");
							$entityPersisterProperty->setAccessible(true);
							$entityPersister = $entityPersisterProperty->getValue($relatedEntity);
							
							$identifierProperty = $reflectionClass->getProperty("_identifier");
							$identifierProperty->setAccessible(true);
							$identifier = $identifierProperty->getValue($relatedEntity);
							
							$entity->$sourceAttribute = $entityPersister->load($identifier, $relatedEntity);
							
							$entity->__isUnserialized__ = true;
						}
						
						if ($operation == AssociationWalker::INSERT_PROXY_IDENTIFIERS && $relatedEntity instanceof Proxy && !$relatedEntity->__isInitialized__) {
							// This configures Doctrine proxies to be Flextrine friendly (basically insert the identifiers into their
							// correct spot and adds a __isUnserialized__ property as the very last property so the AS3 entities can detect
							// when AMF deserialization is finished).
							$reflectionClass = new \ReflectionClass($relatedEntity);
							
							$identifierProperty = $reflectionClass->getProperty("_identifier");
							$identifierProperty->setAccessible(true);
							$identifier = $identifierProperty->getValue($relatedEntity);
							
							foreach ($identifier as $idAttribute => $id)
								$relatedEntity->$idAttribute = $id;
								
							// Make sure the last attribute is __isUnserialized__ so we can use this in Flextrine to determine whether an
							// object has finished unserializing or not.
							$relatedEntity->__isUnserialized__ = true;
						}
						
						// Recurse
						self::_doWalkAssociations($operation, $relatedEntity, $em, $visited);
					}
				}
			}
			
			$entity->__isUnserialized__ = true;
		}
	}
	
	static private function isEntityIdentitied($entity, $em) {
		$entityInsertions = $em->getUnitOfWork()->getScheduledEntityInsertions();
		return (!isset($entityInsertions[spl_object_hash($entity)]));
	}
	
}